  ML I/O ROUTINES

                                  ML I/O ROUTINES
                                 November 9, 1989
                         Programmers' Workshop Conference

     BASIC programmers are spoiled with their OPEN, CLOSE, PRINT, INPUT and
     GET instructions. In order to perform these functions, the micro-
     processor must execute hundreds or even thousands of instructions. So,
     in this conference paper I'm going to give some routines that are
     hundreds of instructions long to help you get started doing I/O in
     machine language. Okay? Well... not exactly. You see, Commodore machine
     language programmers are just a bit pampered too. The C64 has built in
     8K bytes of ROM (read only memory) routines dedicated to performing,
     primarily, input and output from/to other portions of your computer
     system. The C128, in native mode, has 12K bytes of ROM routines for
     this purpose. The I/O routines in this "Kernal" ROM have been made
     general purpose and simple to use so that ML programmers can quickly
     and conveniently code up programs that need I/O. It takes only 10
     instructions in machine language to set up for and perform an OPEN. Two
     instructions for CLOSE, one for GET (three if performing a GET#
     equivalent read from a device), 8 instructions to perform INPUT (10 to
     do the equivalent of INPUT#) and only five to print a literal string
     (7 for print# to a device or file).

     The Kernal -- incidentally, Commodore official terminology for the
     kernel of routines for I/O calls it the "Kernal". The misspelling isn't
     mine. I don't know if it was intentional or a typical programmers'
     misspelling that caused the Kernal to be "the Kernal" instead of "the
     kernel". Anyway -- the Kernal is constructed with a set of high level
     entry pointers in a jump table, all with documented, standardized
     interfaces. The 'jump table' is standard for all Commodore 8 bit
     computers - PET, VIC-20, C64, C128 - so this improves portability,
     promotes use of a library of routines, and simplifies the task of
     writing assembly language code. By using a jump table for the Kernal
     routines, it is possible to maintain constant call addresses for the I/O
     routines even though there may be changes to the routines' location
     within the ROM. The familiar JSR $FFD2 performs a JSR to the instruction
     at that address. This is the jump table entry point to the routine to
     print a character to the current output device. At location $FFD2 there
     is a JMP instruction that transfers control to the actual routine that
     does the work. That routine is located at different places in a C64,
     C128, VIC-20 or PET computer, but the JSR to $FFD2 will always take care
     of the differences.

     Just as call addresses are standard, so are the interfaces to these
     routines. Obviously there are different requirements for parameters for
     different functions, but for a given function the Kernal routine setup
     is the same among different computers, different ROM versions or
     different uses of the function. Any variations required for a particular
     machine is handled by the actual function coding and the programmer is
     relieved of that burden. There is a partial table at the end of this
     paper giving call addresses and interfaces to a few of the common Kernal
     routines for illustration and to get you started. For a complete table,
     refer to a Programmers' Reference Guide. There are good ones for each of
     the Commodore computers published by Commodore or Compute! Publications.
     There are a few things that are generally common among all of the Kernal
     routines, however. All parameters passed to a routine are either
     implicit or contained in the processor registers. Where the actual
     parameters wont fit in the three registers then a pointer to the
     parameters is passed in the registers. Where a function returns a value
     it is always returned in the processor registers. And if an error can
     occur which prevents the function from performing its job, then the
     function returns with the carry flag set and, if possible, an error code
     in the accumulator. This makes error handling relatively simple using
     the Kernal routines.

     To be more specific, and use some examples for illustration, I'll show
     two common problems in ML programming with some assembly language code.
     For simplicity, this is shown in Power Assembler (Buddy) format using
     + and - as temporary labels for forward or backward branching. Labels
     used for Kernal functions are those commonly given in literature and
     shown in the table at the end of this paper. These would be assigned to
     the actual addresses of the routine jump table entries in equate
     statements in your assembler code. First, a simple text reader that
     opens a disk file, prints the text to the screen, then closes the file.
     This routine includes some user control of the output to pause the
     display or abort it (see comments in the code).

     shflag = $28d           ;Address of byte containing flags for shift type
                             ;keys. %001=shift, %010=control, %100=logo key
                             ;Shflag is at location $d3 in the C128.
     seqread = *
             ldx #fname
             lda #namend-fname  ;length of file name calculated by assembler
             jsr setnam      ;Kernal routine
     ;
             lda #8          ;Logical file #8
             tax             ;device #8
             tay             ;secondary address 8
             jsr setlfs      ;Kernal routine
     ;
             jsr open        ;Kernal routine
             bcs error       ;see OPEN for possible error returns
     ;
     ; The file is now open on the disk drive. Next read the data from it
     ; and print
     ;
             ldx #8          ;Logical file #
             jsr chkin       ;Kernal routine
             bcs endread     ;see CHKIN for possible error returns
     readloop = *
             jsr chrin       ;Kernal routine. Get character from serial bus
             bcs endread     ;see CHRIN for error returns
             jsr chrout      ;Kernal routine. Print the character
             jsr readst      ;Kernal routine. Get the status byte for last I/O
             bne endread     ;EOF or disk error causes end
             jsr stop        ;Kernal routine. Check the STOP key
             beq endread     ;and quit if it is pressed
     -       lda shflag      ;check shift, control or logo key pressed
             bne -           ;pause while one of them is held down
             beq readloop    ;unconditional. Continue while not (EOF or STOP)
     endread = *
             jsr clrchn      ;Kernal routine. Clear the bus
             lda #8          ;logical file #
             jsr close       ;Kernal routine.
     error = *
             nop             ;change to BRK for debugging
             rts
     fname  .byte 'testfile' ;name of file to read
     namend = *              ;for calculating name length

     For a second example, I will show how to set up an OPEN to a modem
     channel. This is a commonly asked question in the Programmers' Workshop
     among those just getting started with machine language programming. The
     confusion here is because of the interpretation of what a "name" is for
     a file being opened. The Kernal OPEN routine uses the "name" to contain
     the bytes that are used to set up the RS-232 port if the device number
     is 2, the RS-232 channel. Here is the example.

     RS-232 Open is done by setting the "name" of the file to open to be the
     bytes which set the command and control registers. Then use the normal
     SETNAM, SETLFS and OPEN Kernal calls. For example, to open the file for
     300 baud, full duplex, no handshake, 8 data bits and no parity the
     control register byte is a 6 and the command register byte is 0. Thus
     there is a two character "name" and SETNAM would be called as follows:

     ; OPEN RS-232 channel for I/O
     ;
     rsreg   .byte 6,0       ;the RS-232 control and command register values
     rsopen = *              ;jsr rsopen returns with LFN 2 open to RS-232 port
     ;
             lda #2          ;Length = 2 bytes
             ldx #rsreg
             jsr SETNAM      ;Kernal call
     ;
     ;The RS-232 device number is 2 and there is no significance to the
     ;secondary address that is used in the setup, so the call to SETLFS
     ;would look like:
             lda #2          ;Logical file #2
             tax             ;Device #2
             tay             ;secondary address don't care
             jsr SETLFS      ;Kernal call

     Then a JSR OPEN would complete the process.
    
     SPECIAL CONSIDERATIONS
     ----------------------

     CHRIN vs GETIN for Reading : Use GETIN to get single characters from the
     keyboard with no cursor and for RS-232 input. Use CHRIN for serial bus
     devices (disk) and to perform the equivalent of BASIC's INPUT from the
     keyboard (with flashing cursor and edit keys active). Never use CHRIN
     for RS-232 input. It can lock up your program waiting for a carriage
     return from the RS-232 input and if garbage data is coming in, it can
     overflow the INPUT buffer (88 characters on the C64, 161 characters on
     the C128). Using GETIN on disk I/O just wastes time. For serial bus or
     cassette input the Kernal routine GETIN calls CHRIN, so you might just
     as well use CHRIN. A JSR to CHRIN or GETIN returns one character for
     each call. If you are inputing a string or continuous data you will have
     to store the data away as you read it. Neither of these routines
     performs any buffering, except that CHRIN buffers a logical screen line
     starting at $201 in either the C64 or C128 if the default input device
     (keyboard) is being used.

     Serial bus I/O : You can only use the serial bus for one action at a
     time. So you cannot set up one device for input and another for output.
     For example, to copy a SEQ file from disk to printer it is necessary to
     read input data, clear the channel, set up the output channel, send the
     data, clear the channel then repeat at get data. In ML, with logical
     file 2 for input from disk and logical file 4 for the printer :

     getprint = *          ;DO
             ldx #2          ;Disk drive LFN
             jsr CHKIN       ;Make disk a talker on the bus
             jsr CHRIN       ;Get a character
             pha             ;save character for the moment
             jsr READST      ;Get disk status
             tay             ;save status temporarily in .Y
             jsr CLRCHN      ;Turn off talker (disk drive)
             ldx #4          ;Printer LFN
             jsr CHKOUT      ;Make printer listen
             pla             ;the character we saved
             jsr CHROUT      ;print it
             jsr CLRCHN      ;turn off listener (printer)
             cpy #0          ;where we put the disk status earlier
             beq getprint  ;LOOP until ST  0

     A routine such as this will tend to be a bit slow because of the serial
     bus turn-around on every character. It is more efficient if you BUFFER
     the transfer. To do that, repeat a CHRIN loop that stores characters in
     a buffer until EOF is reached or the buffer is full. Then perform the
     serial bus turnaround and send all the data in the buffer to the printer
     with a CHROUT loop until the buffer is empty. Then, if the last buffer
     full sent to the printer was not cut off because of an EOF when the data
     was read from the disk, go back to the CHRIN loop again until EOF is
     found. The buffer is simply an area of memory that you set aside to hold
     the data temporarily and thus may be used for several different things
     as long as the purpose is temporary storage. A convenient buffer size to
     use is 256 bytes because you can use indexed addressing mode to access
     the buffer. For special cases you may use a different buffer size,
     though. For example, using BURST read on a C128 you may use a buffer
     size of 254 bytes instead as is done in the program "burst read.sda"
     uploaded by John L.

     "Low level" Kernal routines : There are a number of Kernal routines
     available to manipulate the serial bus directly. It is best to avoid the
     use of the low level routines unless you specifically want to manipulate
     the serial bus directly. Using the low level routines can, and usually
     does, create compatibility problems between your program and such
     commonly used devices as fast load cartridges, JiffyDOS, hard disk
     drives, IEEE disk interfaces, and RAMDOS when used with a REU for a
     virtual disk (RAMDISK). Sticking to the high level routines discussed in
     this paper will provide the best compatibility with devices and software
     wedges that many Commodore computer owners like to use. These are often
     used routinely by ML programmers thinking that the time savings will be
     appreciated by the program users. It is a mistake though, because the
     time savings are negligible at ML speeds and the inconvenience of
     compatibility problems is death to many otherwise useful programs.

     Stopping : In most of these routines I have not mentioned any way to
     stop what is happening. It is a simple matter to stick a JSR STOP in
     occasionally at places where it is convenient to code it so that a
     program user can press the STOP key to halt the program. After a
     JSR STOP then a BEQ ABORT can be used to clean up the I/O and go back
     to a higher level control program.

     Reading Nuls : Another special consideration is reading nuls which are
     either a byte with a value of 0 or no byte, depending on where it is
     read from. When doing a CHRIN from a disk file you will ALWAYS get a
     data byte. So a data byte value of 0 means that there is actually a byte
     value 0 at that place in the file. For keyboard input, a GETIN will
     return a 0 value if no key is pressed. You can check for a 0 in the
     accumulator and not do anything with the byte if it is 0. For RS-232,
     however, it is possible to receive no byte in response to a GETIN, or it
     is possible to receive a 0 value byte. Both cases have the accumulator
     set to 0 on return from the JSR GETIN. So with RS-232 I/O you should
     check the RS-232 status register at location 663 ($297) in a C64 or
     location 2580 ($a14) in a C128 after a GETIN. If bit 3 (AND #$08) is a
     %1 then there was no data returned from the last RS-232 GETIN call. If
     this bit is a %0 then the you have valid data. Thus this is a better way
     to check for valid data than to check for a zero byte after the GETIN
     call.

     RS-232 Transmit : A special consideration in sending data to RS-232 is
     that the Kernal RS-232 send routines buffer up to 256 bytes. In ML you
     can easily fill the transmit buffer faster than it can be sent. You
     cannot overflow the buffer, but you can run into problems due to the
     time delay. For example, in Punter protocol you send a block of data 254
     bytes long then wait for the receiving end to acknowledge receiving it.
     If you fill the RS-232 transmit buffer then start a timeout for the
     receiving end to acknowledge it you can get into trouble. At 300 baud it
     takes about 8 seconds to send the full buffer while at 1200 baud it
     takes only about 2 seconds. So a time delay that is right for one wont
     be correct for the other. To avoid this, check bit 0 of the RS-232
     interrupt flag register, location 673 ($2a1) in a C64, 2575 ($a0f) in a
     C128. If it is %1 then data is being sent yet. When the RS-232 transmit
     buffer is empty this bit will change to a %0. So you could avoid the
     time delay problem by starting your timeout delay when bit 0 of the
     interrupt flag register is %0. This bit may also be used to pace output
     of data sent to another computer screen. This allows the receiving end
     to pause or abort the printout without having to wait for a full RS-232
     transmit buffer of data to get sent. You may have experienced this on a
     BBS that was not set up for transmit pacing and it is annoying, but
     easily corrected by proper program design.

     Hopefully this paper has taken some of the mystery out of doing I/O from
     machine language. It looks like a lot of code compared to a simple
     OPEN 8,8,8,"testfile" like you would use from BASIC, and it is. But once
     you have done it one time and have an assembler source file with it then
     it is a simple matter to re-use that code in another program. After
     writing just a few small programs in assembly language you can build a
     library of routines that you will use over and over again, and thus make
     the job easier as you gain experience.


     Here is a table of some of the commonly used Kernal routines.

     Parameter       SETNAM SETLFS OPEN    CHKIN  CHKOUT CHRIN  GETIN  CHROUT
     --------------- ------ ------ ------  ------ ------ ------ ------ ------
     Call address    $ffbd  $ffba  $ffc0   $ffc6  $ffc9  $ffcf  $ffe4  $ffd2
     Parameters      A,X,Y  A,X,Y  none    X      X      A      A      A
     Reg. affected   none   none   A,X,Y   A,X    A,X    A,X    A,X,Y  A
     Prep routines   none   none   SETNAM  OPEN   OPEN   (OPEN) (OPEN) (OPEN)
                                   SETLFS
     Error rtns      none   none   1,2,4,  3,5,6  3,5,7  none   none   none
                                   5,6,240

     Parameter       CLRCHN CLOSE  READST  STOP   PLOT   LOAD   SAVE
     --------------- ------ ------ ------  ------ ------ ------ ------
     Call address    $ffcc  $ffc3  $ffb7   $ffe1  $fff0  $ffd5  $ffd8
     Parameters      none   A      none    none   %c,X,Y A,X,Y  A,X,Y
     Reg. affected   A,X    A,X,Y  A       A,X    A,X,Y  A,X,Y  A,X,Y
     Prep routines   none   none   none    none   none   SETLFS SETLFS
                                                         SETNAM SETNAM
     Error rtns      none   0,240  none    %z=1   none   0,4,5, 5,8,9
                                                         8,9

     ERROR RETURNS : ERROR NUMBER IN .A IF CARRY IS SET ON RETURN
     0      Terminated by STOP key     :  5      Device not present error
     1      Too many open files error  :  6      Not an input file      
     2      File open error            :  7      Not an output file     
     3      File not open error        :  8      File name error        
     4      File not found error       :  9      Illegal device error   
     240    RS-232 buffer allocated/de-allocated (top of BASIC memory moved)

     In addition to these errors, most I/O operations require reading the I/O
     status value to determine the I/O condition after the routine is called.
     Basically, if the routine transfers data or results in serial bus
     activity, then you should execute a JSR READST to check for the results
     of the transfer. A non-zero result indicates an error. Refer to BASIC
     contents of the reserved variable ST to determine the effect of a
     READST. For serial bus I/O you can simply look at the contents of
     location $90. This location is used by the Kernal to store the ST value.
     Here are some brief descriptions of the parameter setup for the Kernal
     routines listed above:

     SETNAM : .A=name length, .X=low byte of name location, .Y=high byte
     SETLFS : .A=logical file #, .X=device, .Y=secondary address
     CHKIN  : .X=logical file #
     CHKOUT : .X=logical file #
     CHRIN  : Returns character in .A (If input device is the keyboard then
              operation of CHRIN is similar to the BASIC INPUT statement.)
     GETIN  : Returns character in .A
     CHROUT : .A=character to send
     CLOSE  : .A=logical file #
     READST : Returns .A = ST value
     STOP   : Returns with the processor Z flag set if STOP is pressed
     PLOT   : Set carry, call with .Y = column, .X = row to set cursor location
              Clear carry and call to return current cursor location in .X, .Y
     LOAD   : .A = 0 for LOAD. To load at the saved address, use any non-zero
              secondary address. If the secondary address is set to 0 then the
              routine will LOAD the program at the address contained in .X, .Y
     SAVE   : .A = ZP ptr to start of SAVE. .X, .Y = end address + 1 of SAVE.








     ----------------------------------------------------------------------

      disclaimer

   The above document is the sole work of the author and is for informational
   and educational purposes. It is intended as a review and should be used as
   such. I except no money, royalties, or gratuities for its contents. I also
   will not be liable for misuse or any damages, either direct or
   consequential, from use of any information found here. ALL INFO IS USE AT
   YOUR OWN RISK !!!

     ----------------------------------------------------------------------

   This page was created using...

   Hot Dog HTML Editor Version 1.0
   NeoPaint Version 3.1c
   Paint Shop Pro 3.1

     ----------------------------------------------------------------------

   Author Email:F.Yarra fyarra@juno.com - comments, suggestions are welcomed.

                     +-----------------------------------+
                     | What's New | About | Links | Home |
                     +-----------------------------------+

     ----------------------------------------------------------------------

      (c)1998 FYARRA
      Last modified November 7, 1998

